// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © BullByte
//@version=6
indicator("Trend Exhaustion & Continuation Map [BullByte]", overlay=false, format=format.price, precision=0, max_bars_back=1000, max_boxes_count=500)

//=============================================================================
// TREND EXHAUSTION & CONTINUATION MAP [BullByte]
//=============================================================================
//
// PURPOSE
// This map follows a directional price leg from its first qualifying drive to
// accepted progress, pullback, recovery, exhaustion, and possible structural failure.
// It is designed to keep those events separate: a strong move is not automatically
// a mature trend, an exhaustion warning is not a reversal, and a continuation event
// is not an instruction to enter a trade.
//
// HOW THE MODEL THINKS ABOUT PRICE
// DRIVE begins only when displacement, a break of prior structure, and directional
// efficiency agree. FORMING and TREND require further accepted progress from the
// confirmed birth close. Time passing by itself cannot promote a leg to a new phase.
//
// Exhaustion is a latched warning based on giveback plus evidence of a failed attempt
// or a sustained stall. It can occur in DRIVE, FORMING, or TREND. Continuation is a
// separate event: after a meaningful pullback, price must reclaim a completed local
// reference in the direction of the active leg. It does not require TREND first.
//
// BALANCE describes a sufficiently wide but inefficient, relatively centred range.
// A release from balance does not, by itself, prove that a new directional leg has
// formed. Likewise, losing protected structure ends the old directional context;
// it does not guarantee that price will continue in the opposite direction.
//
// CONFIRMATION AND DISPLAY
// Permanent state changes and alert events are committed on confirmed bars. Live
// Preview may show a provisional birth diamond and update the active envelope while
// a candle is open; those visuals can change or disappear before that candle closes.
// Confirmed Only holds the display to completed-bar information. Neither setting
// changes the confirmed event rules.
//
// DRAWINGS AND MOVEMENT SCALE
// The outer frame keeps the full observed range of a leg or accepted balance. Inner
// fills show the range built during each directional phase. A dashed transition
// frame preserves context after structure failure or balance release without
// presenting the old direction as active. On intraday charts, a material time gap
// can start a new analytical segment; completed drawings remain on the chart.
//
// IMPORTANT LIMITATIONS AND DISCLAIMER
// This is an analytical charting tool, not an automated trading system, a strategy,
// or investment advice. It does not place orders or model commissions, slippage,
// execution quality, position sizing, or account-level risk. No accuracy, return,
// or profitability is promised. Events can fail and may behave differently across
// symbols, sessions, chart types, and timeframes. Review the logic and validate the
// tool on your own data before relying on its output. You remain responsible for
// every trading decision.
//=============================================================================

//=============================================================================
// 01 - INPUTS
//=============================================================================
// The settings are grouped by the question they control: segment boundaries,
// movement measurement, leg qualification, phase progress, warnings, continuation,
// balance, display, alerts, and the scanner. Inputs marked display.none are still
// available in Settings; they are simply omitted from the chart status line and
// Data Window to keep those areas uncluttered.

string G_GAP = "01 · Segment Boundaries"
string G_SCALE = "02 · Movement Scale"
string G_ENGINE = "03 · Leg Detection"
string G_PHASE = "04 · Phase Transitions"
string G_EXHAUST = "05 · Exhaustion"
string G_TRADE = "06 · Continuation Scan"
string G_BALANCE = "07 · Balance"
string G_VISUAL = "08 · Visual System"
string G_ALERT = "09 · Confirmed Alerts"
string G_DASH = "10 · Scanner"

bool i_resetOnGap = input.bool(true, "Reset Active Leg On Material Time Gap", group=G_GAP, tooltip="On intraday charts, resets active structure when the gap exceeds the threshold below. An isolated missing bar normally will not reset the leg. Completed boxes remain visible. This reset is not applied on daily or higher timeframes.", display=display.none)
float i_gapResetMultiple = input.float(3.0, "Gap Reset Threshold (Chart Bars)", minval=1.5, maxval=20.0, step=0.5, group=G_GAP, tooltip="On intraday charts, the interval between the prior close and the next bar open must exceed this many chart-bar durations. Raise the value to preserve more context across long session breaks.", display=display.none)
string i_scaleMode = input.string("Median True Range (Gap-Filtered)", "Movement Scale", options=["Median True Range (Gap-Filtered)", "Median Close Travel"], group=G_SCALE, tooltip="Choose the movement yardstick used to scale thresholds. Median True Range uses the full true-range sample on ordinary bars; detected intraday gap bars contribute only their own high-low range, so a session jump does not distort the scale. Median Close Travel measures absolute close-to-close movement and excludes samples that span detected chart-time gaps. A leg keeps the scale measured at its birth, so its thresholds do not drift mid-leg.", display=display.none)
int i_scaleLength = input.int(20, "Median Scale Lookback", minval=5, maxval=100, group=G_SCALE, tooltip="Number of samples used by the selected median. On intraday charts, close-travel samples spanning detected time gaps are excluded. During warm-up, the current bar range is used until the median is available.", display=display.none)
int i_driveWindow = input.int(4, "Displacement Window", minval=3, maxval=15, group=G_ENGINE, tooltip="Short windows can detect emerging movement earlier but may create more false starts.", display=display.none)
int i_breakWindow = input.int(4, "Breakout Reference Window", minval=3, maxval=30, group=G_ENGINE, tooltip="The close must exceed the previous reference high or low. The current candle is excluded from that reference.", display=display.none)
float i_birthMoveUnits = input.float(0.85, "Birth Displacement (Scale Units)", minval=0.3, maxval=5.0, step=0.05, group=G_ENGINE, tooltip="Minimum net displacement in selected movement-scale units, combined with the breakout and efficiency conditions.", display=display.none)
float i_birthEfficiency = input.float(0.32, "Minimum Directional Efficiency", minval=0.15, maxval=0.9, step=0.03, group=G_ENGINE, tooltip="Net close displacement divided by the close-to-close path. Lower values can allow earlier detections but may increase false starts.", display=display.none)
int i_protectedWindow = input.int(4, "Initial Protected Structure Window", minval=2, maxval=12, group=G_ENGINE, tooltip="Sets the primary leg-invalidation reference from preceding bars inside the current segment. The reference stays anchored to the leg birth; ordinary continuation triggers do not move it to each newer pullback.", display=display.none)
float i_breakClearanceUnits = input.float(0.12, "Breakout Clearance (Scale Units)", minval=0.0, maxval=1.0, step=0.02, group=G_ENGINE, tooltip="Requires the close to clear the prior breakout level by a small fraction of the active movement scale. Helps reject marginal one-tick breaks across instruments with different prices and tick sizes.", display=display.none)
string i_reversalMode = input.string("Structure Confirmed", "Opposite-Leg Confirmation", options=["Structure Confirmed", "Early Displacement"], group=G_ENGINE, tooltip="Structure Confirmed starts an opposing leg only when its drive condition and the active protected structure break agree. Early Displacement permits a qualifying opposite drive to replace the current leg sooner, with more short-lived flips in chop.", display=display.none)
float i_reversalDisplacementBuffer = input.float(0.30, "Additional Reversal Displacement (Scale Units)", minval=0.0, maxval=2.0, step=0.05, group=G_ENGINE, tooltip="Adds a small displacement requirement only when confirming an opposite leg after protected-structure failure in Structure Confirmed mode. Ordinary drive births keep their existing threshold.", display=display.none)
float i_formingUnits = input.float(0.50, "Forming Extension From Birth", minval=0.1, maxval=3.0, step=0.05, group=G_PHASE, tooltip="Favourable accepted progress beyond the birth close required for FORMING.", display=display.none)
float i_trendUnits = input.float(2.0, "Trend Extension From Birth Close", minval=0.5, maxval=8.0, step=0.1, group=G_PHASE, tooltip="Accepted progress after the confirmed birth close required for TREND. If this value is below the Forming threshold, the effective Trend threshold is kept 0.1 scale units above Forming. It is not a trade-entry prerequisite.", display=display.none)
float effectiveTrendUnits = math.max(i_trendUnits, i_formingUnits + 0.1)
int i_minTrendBars = input.int(3, "Minimum Leg Bars Before Trend", minval=2, maxval=12, group=G_PHASE, tooltip="Minimum completed leg bars before promotion to TREND.", display=display.none)
float i_driveExhaustionUnits = input.float(0.40, "Drive Stall Giveback", minval=0.1, maxval=2.0, step=0.05, group=G_EXHAUST, tooltip="Giveback threshold for detecting an early drive stall.", display=display.none)
float i_trendExhaustionUnits = input.float(0.70, "Forming / Trend Giveback", minval=0.1, maxval=3.0, step=0.05, group=G_EXHAUST, tooltip="Giveback threshold during FORMING and TREND. The warning remains independent of trend phase.", display=display.none)
float i_failedAttemptBuffer = input.float(0.05, "Failed Extension Buffer", minval=0.0, maxval=0.5, step=0.01, group=G_EXHAUST, tooltip="Minimum excursion beyond the best accepted close required for the failed-extension route to qualify.", display=display.none)
int i_exhaustionStallBars = input.int(2, "Bars Without New Accepted Progress", minval=1, maxval=8, group=G_EXHAUST, tooltip="Alternative exhaustion route: sufficient giveback plus this many consecutive bars without new accepted progress.", display=display.none)
float i_exhaustionRearmUnits = input.float(0.75, "New Progress Required To Rearm", minval=0.2, maxval=3.0, step=0.05, group=G_EXHAUST, tooltip="The yellow exhaustion event is latched. It rearms only after meaningful accepted progress beyond the close recorded when the warning occurred.", display=display.none)
int i_triggerWindow = input.int(2, "Local Reclaim Reference", minval=1, maxval=5, group=G_TRADE, tooltip="A continuation trigger requires a close through this many preceding bars' reference high or low, after a sufficient pullback.", display=display.none)
float i_minPullbackUnits = input.float(0.25, "Minimum Pullback Depth", minval=0.05, maxval=2.0, step=0.05, group=G_TRADE, tooltip="Minimum retracement from the best accepted close, measured in scale units, required before a continuation setup can qualify.", display=display.none)
float i_givebackPhaseUnits = input.float(0.35, "Giveback Phase Threshold", minval=0.1, maxval=2.0, step=0.05, group=G_TRADE, tooltip="Distance away from the best accepted close required to enter GIVEBACK.", display=display.none)
float i_progressMilestoneUnits = input.float(1.0, "Accepted Progress Milestone", minval=0.2, maxval=4.0, step=0.1, group=G_TRADE, tooltip="Additional favourable progress required between successive oscillator milestones.", display=display.none)
int i_balanceWindow = input.int(12, "Balance Observation Window", minval=5, maxval=30, group=G_BALANCE, tooltip="Full segment-local history is required before balance can be accepted.", display=display.none)
float i_balanceEfficiency = input.float(0.24, "Maximum Balance Efficiency", minval=0.08, maxval=0.5, step=0.02, group=G_BALANCE, tooltip="Maximum net displacement relative to the close-to-close path.", display=display.none)
float i_balanceWidthUnits = input.float(2.0, "Minimum Balance Width", minval=0.3, maxval=8.0, step=0.1, group=G_BALANCE, tooltip="Minimum range width in movement-scale units.", display=display.none)
int i_balancePersistence = input.int(4, "Required Balance Bars", minval=2, maxval=10, group=G_BALANCE, tooltip="Number of consecutive completed bars that must meet the balance conditions.", display=display.none)
float i_balanceBreakUnits = input.float(0.10, "Balance Release Buffer", minval=0.0, maxval=1.0, step=0.05, group=G_BALANCE, tooltip="Required close beyond a stored balance boundary to release balance. Release does not guarantee a new trend.", display=display.none)
int i_balanceMaturity = input.int(8, "Established Balance Duration", minval=4, maxval=20, group=G_BALANCE, tooltip="Number of confirmed bars spent in an accepted balance before its grey fill becomes more prominent. Separate from the balance-detection persistence requirement.", display=display.none)
int i_keepSegments = input.int(100, "Historical Drawing Capacity", minval=10, maxval=180, group=G_VISUAL, tooltip="Retention budget for outer leg containers, inner phase-colour bands, and balance boxes. Oldest drawings are removed first to stay within TradingView object limits.", display=display.none)
bool i_showPriceEvents = input.bool(true, "Show Price-Chart Events", group=G_VISUAL, tooltip="Shows confirmed drive-birth, exhaustion, balance, and protected-structure-failure markers. Continuation triggers appear in the oscillator pane and can be included in the consolidated confirmed alert.", display=display.none)
bool i_showPaneEvents = input.bool(true, "Show Oscillator Event Markers", group=G_VISUAL, tooltip="Shows small pane markers for accepted progress, continuation, and exhaustion.", display=display.none)
bool i_showGiveback = input.bool(true, "Show Giveback Field", group=G_VISUAL, tooltip="Displays a neutral grey field that expands as price gives back progress.", display=display.none)
string i_displayMode = input.string("Live Preview", "Display Mode", options=["Live Preview", "Confirmed Only"], group=G_VISUAL, tooltip="Live Preview lets the open candle update the provisional birth marker, active envelope, giveback field, and scanner distances. These visuals can change before close. Confirmed Only holds the display to completed-bar information. Neither mode changes confirmed state transitions or alert timing.", display=display.none)
bool i_livePreviewEnabled = i_displayMode == "Live Preview"

// One alert() pathway replaces the individual alertcondition menu items.
// Select which event families the single alert should include.
bool i_alertEnabled = input.bool(true, "Enable Confirmed Alerts", group=G_ALERT, tooltip="Create one TradingView alert using 'Any alert() function call'. Only confirmed bar-close events are sent.", display=display.none)
bool i_alertBirths = input.bool(true, "Drive Births", group=G_ALERT, tooltip="Include confirmed bullish and bearish drive-birth events.", display=display.none)
bool i_alertPhases = input.bool(true, "Phase Changes", group=G_ALERT, tooltip="Include confirmed FORMING and TREND phase transitions.", display=display.none)
bool i_alertContinuations = input.bool(true, "Continuation Triggers", group=G_ALERT, tooltip="Include confirmed long and short continuation triggers.", display=display.none)
bool i_alertWarnings = input.bool(true, "Exhaustion and Structure Failures", group=G_ALERT, tooltip="Include exhaustion warnings and protected-structure failures.", display=display.none)
bool i_alertBalance = input.bool(true, "Balance Events", group=G_ALERT, tooltip="Include accepted balance and balance-release events.", display=display.none)

bool i_showDashboard = input.bool(true, "Show Scanner", group=G_DASH, display=display.none)
string i_dashboardPosition = input.string("Top Right", "Scanner Position", options=["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group=G_DASH, display=display.none)

//=============================================================================
// 02 - COLOURS AND SMALL HELPERS
//=============================================================================

color C_BULL_DRIVE = color.rgb(165, 255, 207)
color C_BULL_FORMING = color.rgb(0, 205, 118)
color C_BULL_TREND = color.rgb(0, 118, 72)
color C_BEAR_DRIVE = color.rgb(255, 190, 198)
color C_BEAR_FORMING = color.rgb(245, 42, 70)
color C_BEAR_TREND = color.rgb(170, 8, 39)
color C_BULL_FRAME = color.rgb(66, 220, 151)
color C_BEAR_FRAME = color.rgb(255, 77, 99)
color C_GREY = color.rgb(132, 145, 160)
color C_YELLOW = color.rgb(255, 214, 64)
color C_DARK = color.rgb(16, 23, 32)
color C_HEAD = color.rgb(29, 40, 52)
color C_TEXT = color.rgb(235, 241, 247)
color C_MUTED = color.rgb(151, 165, 181)

f_phaseColor(int _direction, int _phase) =>
    _direction == 1 ? (_phase == 1 ? C_BULL_DRIVE : _phase == 2 ? C_BULL_FORMING : C_BULL_TREND) :
      _direction == -1 ? (_phase == 1 ? C_BEAR_DRIVE : _phase == 2 ? C_BEAR_FORMING : C_BEAR_TREND) :
      C_GREY

f_phaseName(int _direction, int _phase) =>
    _direction == 1 ? (_phase == 1 ? "BULLISH DRIVE" : _phase == 2 ? "BULLISH FORMING" : "BULLISH TREND") :
      _direction == -1 ? (_phase == 1 ? "BEARISH DRIVE" : _phase == 2 ? "BEARISH FORMING" : "BEARISH TREND") :
      _direction == 2 ? "BALANCE" : "SCANNING"

f_phaseTransparency(int _phase) =>
    _phase == 1 ? 79 : _phase == 2 ? 77 : 74

f_dashboardPosition(string _position) =>
    switch _position
        "Top Left" => position.top_left
        "Bottom Right" => position.bottom_right
        "Bottom Left" => position.bottom_left
        => position.top_right

f_intensity(float _units) =>
    100.0 * (1.0 - math.exp(-math.max(_units, 0.0)))

//=============================================================================
// 03 - GAP HANDLING AND MOVEMENT SCALE
//
// Thresholds are expressed in units of typical movement rather than fixed price
// amounts, so the same settings can be used on instruments with different prices.
// Median True Range measures the typical high-low range, including ordinary gaps
// within a bar. Median Close Travel measures typical absolute close-to-close change.
// On intraday charts, samples that span a detected time gap are treated specially:
// a gap bar contributes its own high-low range, while close-travel skips that sample.
//
// The selected scale is frozen when a leg is born. That keeps the leg's thresholds
// consistent instead of allowing the yardstick to change in the middle of the move.
// Gap-reset logic is intraday-only in this version; daily and higher charts do not
// start a fresh segment through this setting.
//=============================================================================

bool rawGapDetected = timeframe.isintraday and not na(time_close[1]) and time > time_close[1]
float chartSeconds = float(timeframe.in_seconds())
float gapSeconds = not na(time_close[1]) ? math.max(0.0, (time - time_close[1]) / 1000.0) : 0.0
bool materialGapDetected = rawGapDetected and not na(chartSeconds) and gapSeconds > chartSeconds * i_gapResetMultiple
bool segmentStarts = barstate.isfirst or (i_resetOnGap and materialGapDetected)
float rangeSample = na(close[1]) or rawGapDetected ? high - low : math.max(high - low, math.max(math.abs(high - close[1]), math.abs(low - close[1])))
float closeTravelSample = na(close[1]) or rawGapDetected ? na : math.max(math.abs(close - close[1]), syminfo.mintick)
rangeSample := math.max(rangeSample, syminfo.mintick)
float medianRangeScale = ta.percentile_nearest_rank(rangeSample, i_scaleLength, 50)
float medianCloseScale = ta.percentile_nearest_rank(closeTravelSample, i_scaleLength, 50)
var int barsInSegment = 0
float chosenScale = i_scaleMode == "Median Close Travel" ? medianCloseScale : medianRangeScale
// Keep early chart history usable while the selected median is warming up.
// Once a median is available, it remains the only selected movement estimate.
float safeScale = na(chosenScale) or chosenScale <= syminfo.mintick ? rangeSample : chosenScale
safeScale := math.max(safeScale, syminfo.mintick)

//=============================================================================
// 04 - LOCAL PRICE REFERENCES
//
// These helpers stop at the current segment boundary, so a material gap cannot
// make a new leg inherit a breakout level from the previous segment. Reference
// highs and lows use completed bars only; the current candle must prove its own break.
// Directional efficiency compares net close displacement with the path travelled.
// A straight move scores more efficiently than a similar net move made through chop.
//=============================================================================

f_segmentHighest(int _window, int _availablePriorBars) =>
    float result = na
    int n = math.min(_window, _availablePriorBars)
    if n > 0
        result := high[1]
        if n > 1
            for j = 2 to n
                result := math.max(result, high[j])
    result

f_segmentLowest(int _window, int _availablePriorBars) =>
    float result = na
    int n = math.min(_window, _availablePriorBars)
    if n > 0
        result := low[1]
        if n > 1
            for j = 2 to n
                result := math.min(result, low[j])
    result

f_segmentPath(int _window, int _availablePriorBars) =>
    float result = 0.0
    int n = math.min(_window, _availablePriorBars)
    if n > 0
        for j = 0 to n - 1
            result += math.abs(close[j] - close[j + 1])
    result

int availablePriorBars = segmentStarts ? 0 : barsInSegment
float driveBaseClose = availablePriorBars >= i_driveWindow ? close[i_driveWindow] : na
float drivePath = f_segmentPath(i_driveWindow, availablePriorBars)
float driveNet = not na(driveBaseClose) ? close - driveBaseClose : na
float directionalEfficiency = not na(driveNet) and drivePath > syminfo.mintick ? driveNet / drivePath : 0.0
float priorBreakHigh = f_segmentHighest(i_breakWindow, availablePriorBars)
float priorBreakLow = f_segmentLowest(i_breakWindow, availablePriorBars)
float initialProtectedLow = f_segmentLowest(i_protectedWindow, availablePriorBars)
float initialProtectedHigh = f_segmentHighest(i_protectedWindow, availablePriorBars)
float priorTriggerHigh = f_segmentHighest(i_triggerWindow, availablePriorBars)
float priorTriggerLow = f_segmentLowest(i_triggerWindow, availablePriorBars)
int birthHistoryNeeded = math.max(i_driveWindow, math.max(i_breakWindow, i_protectedWindow))
bool birthHistoryReady = availablePriorBars >= birthHistoryNeeded and not na(driveNet) and drivePath > syminfo.mintick
float breakoutClearance = safeScale * i_breakClearanceUnits
// A drive must combine displacement, a clear break of prior structure, directional
// efficiency, and a close in the direction of travel. No single test is sufficient.
bool bullishBirthRaw = not segmentStarts and birthHistoryReady and not na(priorBreakHigh) and close > priorBreakHigh + breakoutClearance and driveNet >= safeScale * i_birthMoveUnits and directionalEfficiency >= i_birthEfficiency and close > open
bool bearishBirthRaw = not segmentStarts and birthHistoryReady and not na(priorBreakLow) and close < priorBreakLow - breakoutClearance and driveNet <= -safeScale * i_birthMoveUnits and directionalEfficiency <= -i_birthEfficiency and close < open
// Opposite legs use a slightly higher displacement threshold in Structure Confirmed
// mode. This is a detection filter for marginal reversals, not a drawing filter:
// once a reversal qualifies, its box and events remain visible even if it later fails.
bool bullishReversalBirthRaw = bullishBirthRaw and driveNet >= safeScale * (i_birthMoveUnits + i_reversalDisplacementBuffer)
bool bearishReversalBirthRaw = bearishBirthRaw and driveNet <= -safeScale * (i_birthMoveUnits + i_reversalDisplacementBuffer)

//=============================================================================
// 05 - BALANCE DETECTION
//
// Balance is not simply a narrow range. The script requires enough local history,
// a minimum range width, limited net progress relative to the close-to-close path,
// and a close that remains reasonably near the range's centre. Those conditions
// must persist for the configured number of completed bars before balance is accepted.
//=============================================================================

float balanceHigh = f_segmentHighest(i_balanceWindow, availablePriorBars)
float balanceLow = f_segmentLowest(i_balanceWindow, availablePriorBars)
float balanceWidth = balanceHigh - balanceLow
float balanceMid = (balanceHigh + balanceLow) * 0.5
float balancePath = f_segmentPath(i_balanceWindow, availablePriorBars)
float balanceBaseClose = availablePriorBars >= i_balanceWindow ? close[i_balanceWindow] : na
float balanceNet = not na(balanceBaseClose) ? math.abs(close - balanceBaseClose) : na
float balanceEfficiency = not na(balanceNet) and balancePath > syminfo.mintick ? balanceNet / balancePath : 1.0
bool balanceHistoryReady = availablePriorBars >= i_balanceWindow
bool balanceWidthReady = not na(balanceWidth) and balanceWidth >= safeScale * i_balanceWidthUnits
bool balanceCentred = not na(balanceWidth) and balanceWidth > 0 and math.abs(close - balanceMid) <= balanceWidth * 0.35
bool balanceRaw = not segmentStarts and balanceHistoryReady and not na(balanceHigh) and not na(balanceLow) and balanceWidthReady and balancePath > syminfo.mintick and balanceCentred and balanceEfficiency <= i_balanceEfficiency
var int balanceBars = 0
var int balanceAge = 0
int currentBalanceRun = balanceRaw ? (segmentStarts ? 1 : balanceBars + 1) : 0
bool balanceConfirmed = currentBalanceRun >= i_balancePersistence

//=============================================================================
// 06 - STATE ENGINE
//
// One set of state variables drives the pane, scanner, drawings, and alerts.
// regime: +1 bullish, -1 bearish, 0 transition, 2 balance.
// trendPhase: 1 DRIVE, 2 FORMING, 3 TREND; advancement requires accepted close progress.
// legPhase: 0 DRIVE, 1 GIVEBACK, 2 RECOVERY; this describes the current retracement cycle.
// A wick can help define a pullback extreme, but cannot promote a phase on its own.
//=============================================================================

var int regime = 0
var bool transitionActive = false
var int transitionBars = 0
// After a protected-structure failure, remember which direction would replace
// the failed leg. This prevents a routine birth condition from immediately
// starting a small leg in the old direction while the engine is transitioning.
var int transitionExpectedDirection = 0
var int trendPhase = 0
var int legPhase = 0
var int legBars = 0
var float legBirthClose = na
var float scaleAtBirth = na
var float bestAcceptedClose = na
// Primary protected structure is anchored at leg birth. Keeping it fixed avoids
// treating every small continuation swing as a complete reversal of the broader leg.
var float protectedLow = na
var float protectedHigh = na
var bool pullbackActive = false
var float pullbackExtreme = na
var float pullbackReferenceClose = na
var int barsSinceAcceptedClose = 0
var float lastMilestoneClose = na
var bool exhaustionLatched = false
var float exhaustionReferenceClose = na
var string exhaustionStatus = "-"
var float lockedBalanceHigh = na
var float lockedBalanceLow = na
var string lastEvent = "Waiting for valid history"

// These event flags are one-bar events, not permanent state labels. The confirmed
// engine sets them when a defined transition occurs; the plots, scanner, and alert
// message all read the same flags to avoid maintaining separate versions of the logic.
bool bullBirthEvent = false
bool bearBirthEvent = false
bool bullFailureEvent = false
bool bearFailureEvent = false
bool balanceBirthEvent = false
bool balanceReleaseEvent = false
bool bullFormingEvent = false
bool bearFormingEvent = false
bool bullTrendEvent = false
bool bearTrendEvent = false
bool bullContinuationEvent = false
bool bearContinuationEvent = false
bool exhaustionEvent = false
bool givebackStartEvent = false
bool recoveryStartEvent = false
bool resumedDriveEvent = false
bool progressMilestoneEvent = false
float bullFailurePrice = na
float bearFailurePrice = na
float exhaustionPrice = na
float driveImpulse = na

bool bullishStructureLost = regime == 1 and not na(protectedLow) and close < protectedLow
bool bearishStructureLost = regime == -1 and not na(protectedHigh) and close > protectedHigh
bool bullishOppositeCandidate = i_reversalMode == "Early Displacement" ? bullishBirthRaw : bullishReversalBirthRaw
bool bearishOppositeCandidate = i_reversalMode == "Early Displacement" ? bearishBirthRaw : bearishReversalBirthRaw
bool bullishTransitionCandidate = transitionActive and transitionExpectedDirection == 1 and i_reversalMode != "Early Displacement" ? bullishReversalBirthRaw : bullishBirthRaw
bool bearishTransitionCandidate = transitionActive and transitionExpectedDirection == -1 and i_reversalMode != "Early Displacement" ? bearishReversalBirthRaw : bearishBirthRaw
bool bullishAllowedFromTransition = not transitionActive or transitionExpectedDirection == 0 or transitionExpectedDirection == 1
bool bearishAllowedFromTransition = not transitionActive or transitionExpectedDirection == 0 or transitionExpectedDirection == -1
bool liveBullPreview = i_livePreviewEnabled and barstate.islast and not barstate.isconfirmed and not segmentStarts and ((regime == 0 and bullishAllowedFromTransition and bullishTransitionCandidate) or (regime == 2 and bullishBirthRaw) or (regime == -1 and bullishOppositeCandidate and (i_reversalMode == "Early Displacement" or bearishStructureLost)))
bool liveBearPreview = i_livePreviewEnabled and barstate.islast and not barstate.isconfirmed and not segmentStarts and ((regime == 0 and bearishAllowedFromTransition and bearishTransitionCandidate) or (regime == 2 and bearishBirthRaw) or (regime == 1 and bearishOppositeCandidate and (i_reversalMode == "Early Displacement" or bullishStructureLost)))

if barstate.isconfirmed
    if segmentStarts
        regime := 0
        trendPhase := 0
        legPhase := 0
        legBars := 0
        legBirthClose := na
        scaleAtBirth := na
        bestAcceptedClose := na
        protectedLow := na
        protectedHigh := na
        pullbackActive := false
        pullbackExtreme := na
        pullbackReferenceClose := na
        barsSinceAcceptedClose := 0
        lastMilestoneClose := na
        exhaustionLatched := false
        exhaustionReferenceClose := na
        exhaustionStatus := "-"
        lockedBalanceHigh := na
        lockedBalanceLow := na
        balanceBars := 0
        balanceAge := 0
        barsInSegment := 1
        transitionActive := false
        transitionBars := 0
        transitionExpectedDirection := 0
        if materialGapDetected and i_resetOnGap
            lastEvent := "INTRADAY GAP RESET"
    else
        barsInSegment += 1
        balanceBars := currentBalanceRun
        int oldRegime = regime
        int nextRegime = oldRegime

        if oldRegime == 0
            // Following a structural failure, only the opposite direction may
            // replace the failed leg. In Structure Confirmed mode it must also
            // clear the additional displacement threshold; normal scanning and
            // balance releases retain the ordinary birth criteria.
            bool directedRecovery = transitionActive and transitionExpectedDirection != 0
            bool bullishRecoveryQualified = transitionExpectedDirection == 1 and (i_reversalMode == "Early Displacement" ? bullishBirthRaw : bullishReversalBirthRaw)
            bool bearishRecoveryQualified = transitionExpectedDirection == -1 and (i_reversalMode == "Early Displacement" ? bearishBirthRaw : bearishReversalBirthRaw)
            if directedRecovery
                if bullishRecoveryQualified
                    nextRegime := 1
                else if bearishRecoveryQualified
                    nextRegime := -1
                else if balanceConfirmed
                    nextRegime := 2
            else
                if bullishBirthRaw
                    nextRegime := 1
                else if bearishBirthRaw
                    nextRegime := -1
                else if balanceConfirmed
                    nextRegime := 2
        else if oldRegime == 1
            bool bearishReversalQualified = i_reversalMode == "Early Displacement" ? bearishBirthRaw : bearishReversalBirthRaw
            if bearishReversalQualified and (i_reversalMode == "Early Displacement" or bullishStructureLost)
                nextRegime := -1
                if bullishStructureLost
                    bullFailureEvent := true
                    bullFailurePrice := protectedLow
            else if bullishStructureLost
                nextRegime := 0
                bullFailureEvent := true
                bullFailurePrice := protectedLow
            else if balanceConfirmed
                nextRegime := 2
        else if oldRegime == -1
            bool bullishReversalQualified = i_reversalMode == "Early Displacement" ? bullishBirthRaw : bullishReversalBirthRaw
            if bullishReversalQualified and (i_reversalMode == "Early Displacement" or bearishStructureLost)
                nextRegime := 1
                if bearishStructureLost
                    bearFailureEvent := true
                    bearFailurePrice := protectedHigh
            else if bearishStructureLost
                nextRegime := 0
                bearFailureEvent := true
                bearFailurePrice := protectedHigh
            else if balanceConfirmed
                nextRegime := 2
        else if oldRegime == 2
            bool balanceBreakUp = not na(lockedBalanceHigh) and close > lockedBalanceHigh + safeScale * i_balanceBreakUnits
            bool balanceBreakDown = not na(lockedBalanceLow) and close < lockedBalanceLow - safeScale * i_balanceBreakUnits
            if bullishBirthRaw
                nextRegime := 1
            else if bearishBirthRaw
                nextRegime := -1
            else if balanceBreakUp or balanceBreakDown
                nextRegime := 0
                balanceReleaseEvent := true

        if nextRegime != oldRegime
            regime := nextRegime
            if nextRegime == 1
                transitionActive := false
                transitionBars := 0
                transitionExpectedDirection := 0
                legBirthClose := close
                scaleAtBirth := safeScale
                bestAcceptedClose := close
                protectedLow := initialProtectedLow
                protectedHigh := na
                pullbackActive := false
                pullbackExtreme := na
                pullbackReferenceClose := na
                trendPhase := 1
                legPhase := 0
                legBars := 1
                barsSinceAcceptedClose := 0
                lastMilestoneClose := close
                exhaustionLatched := false
                exhaustionReferenceClose := na
                exhaustionStatus := "CLEAR"
                lockedBalanceHigh := na
                lockedBalanceLow := na
                bullBirthEvent := true
                driveImpulse := f_intensity(math.max(0.0, (close - close[1]) / scaleAtBirth))
                lastEvent := oldRegime == -1 ? "BULLISH REVERSAL" : oldRegime == 2 ? "BALANCE BREAK UP" : "BULLISH DRIVE"
                if oldRegime == 2
                    balanceReleaseEvent := true
            else if nextRegime == -1
                transitionActive := false
                transitionBars := 0
                transitionExpectedDirection := 0
                legBirthClose := close
                scaleAtBirth := safeScale
                bestAcceptedClose := close
                protectedHigh := initialProtectedHigh
                protectedLow := na
                pullbackActive := false
                pullbackExtreme := na
                pullbackReferenceClose := na
                trendPhase := 1
                legPhase := 0
                legBars := 1
                barsSinceAcceptedClose := 0
                lastMilestoneClose := close
                exhaustionLatched := false
                exhaustionReferenceClose := na
                exhaustionStatus := "CLEAR"
                lockedBalanceHigh := na
                lockedBalanceLow := na
                bearBirthEvent := true
                driveImpulse := f_intensity(math.max(0.0, (close[1] - close) / scaleAtBirth))
                lastEvent := oldRegime == 1 ? "BEARISH REVERSAL" : oldRegime == 2 ? "BALANCE BREAK DOWN" : "BEARISH DRIVE"
                if oldRegime == 2
                    balanceReleaseEvent := true
            else if nextRegime == 2
                transitionActive := false
                transitionBars := 0
                transitionExpectedDirection := 0
                lockedBalanceHigh := balanceHigh
                lockedBalanceLow := balanceLow
                pullbackActive := false
                pullbackExtreme := na
                pullbackReferenceClose := na
                trendPhase := 0
                legPhase := 0
                exhaustionLatched := false
                exhaustionReferenceClose := na
                exhaustionStatus := "-"
                balanceBirthEvent := true
                lastEvent := oldRegime == 0 ? "BALANCE FORMED" : "LEG ENTERED BALANCE"
            else
                trendPhase := 0
                legPhase := 0
                pullbackActive := false
                pullbackExtreme := na
                pullbackReferenceClose := na
                if bullFailureEvent
                    exhaustionLatched := true
                    exhaustionStatus := "STRUCTURE FAILED"
                    lastEvent := "BULL STRUCTURE FAILED"
                    transitionActive := true
                    transitionBars := 1
                    transitionExpectedDirection := -1
                else if bearFailureEvent
                    exhaustionLatched := true
                    exhaustionStatus := "STRUCTURE FAILED"
                    lastEvent := "BEAR STRUCTURE FAILED"
                    transitionActive := true
                    transitionBars := 1
                    transitionExpectedDirection := 1
                else
                    exhaustionStatus := "-"
                    if oldRegime == 2
                        lastEvent := "BALANCE RELEASED"
                        transitionActive := true
                        transitionBars := 1
                        transitionExpectedDirection := 0
                lockedBalanceHigh := na
                lockedBalanceLow := na
        else
            regime := nextRegime
            if regime == 0 and transitionActive
                transitionBars += 1

            if regime == 1 or regime == -1
                legBars += 1
                int phaseAtBarStart = trendPhase
                float priorBestClose = bestAcceptedClose
                bool madeNewAcceptedClose = regime == 1 ? close > priorBestClose : close < priorBestClose
                bool continuationTriggered = false

                // Continuation requires a meaningful pullback and a reclaim of
                // a completed local reference. It is separate from trend maturity.
                if pullbackActive
                    float candidateExtreme = regime == 1 ? math.min(pullbackExtreme, low) : math.max(pullbackExtreme, high)
                    float pullbackDepthUnits = regime * (pullbackReferenceClose - candidateExtreme) / scaleAtBirth
                    bool localReclaim = regime == 1 ? (not na(priorTriggerHigh) and close > priorTriggerHigh) : (not na(priorTriggerLow) and close < priorTriggerLow)
                    if pullbackDepthUnits >= i_minPullbackUnits and localReclaim
                        continuationTriggered := true
                        if regime == 1
                            bullContinuationEvent := true
                            // Keep the primary bullish leg guard anchored at birth.
                            // The pullback extreme qualifies the continuation, but does not
                            // redefine the boundary whose failure ends the broader leg.
                            lastEvent := "LONG CONTINUATION TRIGGER"
                        else
                            bearContinuationEvent := true
                            // Keep the primary bearish leg guard anchored at birth.
                            // The pullback extreme qualifies the continuation, but does not
                            // redefine the boundary whose failure ends the broader leg.
                            lastEvent := "SHORT CONTINUATION TRIGGER"
                        pullbackActive := false
                        pullbackExtreme := na
                        pullbackReferenceClose := na
                        legPhase := 0
                    else
                        pullbackExtreme := candidateExtreme
                        bool responseTowardTrend = regime == 1 ? close > close[1] : close < close[1]
                        if legPhase == 1 and responseTowardTrend
                            legPhase := 2
                            recoveryStartEvent := true
                            if not madeNewAcceptedClose
                                lastEvent := "RECOVERY DEVELOPING"
                else if not madeNewAcceptedClose
                    bool firstAdverseClose = regime == 1 ? close < close[1] : close > close[1]
                    if firstAdverseClose
                        pullbackActive := true
                        pullbackExtreme := regime == 1 ? low : high
                        pullbackReferenceClose := bestAcceptedClose

                // Accepted progress uses closes, not wick-only new extremes.
                if madeNewAcceptedClose
                    if pullbackActive and not continuationTriggered
                        pullbackActive := false
                        pullbackExtreme := na
                        pullbackReferenceClose := na
                    float newExtensionUnits = math.max(0.0, regime * (close - priorBestClose) / scaleAtBirth)
                    driveImpulse := f_intensity(newExtensionUnits)
                    if not na(lastMilestoneClose) and regime * (close - lastMilestoneClose) >= scaleAtBirth * i_progressMilestoneUnits
                        progressMilestoneEvent := true
                        lastMilestoneClose := close
                    if exhaustionLatched and not na(exhaustionReferenceClose)
                        float recoveryFromExhaustion = regime * (close - exhaustionReferenceClose) / scaleAtBirth
                        if recoveryFromExhaustion >= i_exhaustionRearmUnits
                            exhaustionLatched := false
                            exhaustionReferenceClose := na
                            exhaustionStatus := "CLEARED"
                            lastEvent := "EXHAUSTION CLEARED BY PROGRESS"
                    bestAcceptedClose := close
                    barsSinceAcceptedClose := 0
                    if legPhase != 0
                        legPhase := 0
                        resumedDriveEvent := true
                        if not bullContinuationEvent and not bearContinuationEvent
                            lastEvent := "ACCEPTED PROGRESS RESUMED"
                else
                    barsSinceAcceptedClose += 1
                    float givebackUnitsLocal = math.max(0.0, regime * (priorBestClose - close) / scaleAtBirth)
                    // A continuation trigger owns its candle; do not immediately
                    // overwrite it with a new GIVEBACK transition on that bar.
                    if not continuationTriggered and legPhase == 0 and givebackUnitsLocal >= i_givebackPhaseUnits
                        legPhase := 1
                        givebackStartEvent := true
                        lastEvent := "GIVEBACK STARTED"

                float extensionFromBirth = regime * (bestAcceptedClose - legBirthClose) / scaleAtBirth
                // FORMING and TREND are progress thresholds, not time-based labels.
                if trendPhase == 1 and extensionFromBirth >= i_formingUnits
                    trendPhase := 2
                    if regime == 1
                        bullFormingEvent := true
                        if not bullContinuationEvent and not bearContinuationEvent
                            lastEvent := "BULLISH FORMING"
                    else
                        bearFormingEvent := true
                        if not bullContinuationEvent and not bearContinuationEvent
                            lastEvent := "BEARISH FORMING"
                else if trendPhase == 2 and legBars >= i_minTrendBars and extensionFromBirth >= effectiveTrendUnits
                    trendPhase := 3
                    if regime == 1
                        bullTrendEvent := true
                        if not bullContinuationEvent and not bearContinuationEvent
                            lastEvent := "BULLISH TREND"
                    else
                        bearTrendEvent := true
                        if not bullContinuationEvent and not bearContinuationEvent
                            lastEvent := "BEARISH TREND"

                // Exhaustion is a latched warning; it does not establish a reversal.
                if not madeNewAcceptedClose and not exhaustionLatched and not continuationTriggered
                    float currentGivebackUnits = math.max(0.0, regime * (priorBestClose - close) / scaleAtBirth)
                    bool failedExtension = regime == 1 ? high > priorBestClose + scaleAtBirth * i_failedAttemptBuffer and close <= priorBestClose : low < priorBestClose - scaleAtBirth * i_failedAttemptBuffer and close >= priorBestClose
                    bool sustainedStall = barsSinceAcceptedClose >= i_exhaustionStallBars
                    float phaseGivebackThreshold = phaseAtBarStart == 1 ? i_driveExhaustionUnits : i_trendExhaustionUnits
                    if currentGivebackUnits >= phaseGivebackThreshold and (failedExtension or sustainedStall)
                        exhaustionEvent := true
                        exhaustionLatched := true
                        exhaustionReferenceClose := priorBestClose
                        exhaustionStatus := phaseAtBarStart == 1 ? "DRIVE STALL" : "EXHAUSTION WATCH"
                        exhaustionPrice := regime == 1 ? high : low
                        lastEvent := exhaustionStatus
                if exhaustionStatus == "CLEARED" and not exhaustionEvent
                    exhaustionStatus := "CLEAR"
        balanceAge := regime == 2 ? (oldRegime == 2 ? balanceAge + 1 : 1) : 0

//=============================================================================
// 07 - PRICE CONTEXT ENVELOPES AND PHASE BANDS
//
// The outer frame records the full high-low range observed while a leg or balance is
// active. A coloured inner band records the range built during each DRIVE, FORMING,
// or TREND phase; when the phase changes, that band is frozen and a new one begins.
//
// After protected structure fails, or balance is released without a qualifying new
// leg, the map can show a hollow dashed transition frame. It preserves recent context
// without suggesting that the former direction is still active. Two confirmed neutral
// bars are required before a transition frame is drawn. A material time gap resets the
// active drawing segment, but does not erase the historical boxes already completed.
//=============================================================================

int balanceVisualPhase = regime == 2 ? (balanceAge >= i_balanceMaturity ? 3 : 2) : 0
int visualKind = regime == 0 and transitionActive and transitionBars >= 2 ? 3 : regime
int visualPhase = regime == 1 or regime == -1 ? trendPhase : regime == 2 ? balanceVisualPhase : 0
int visualKey = visualKind == 1 ? 10 + visualPhase : visualKind == -1 ? -10 - visualPhase : visualKind == 2 ? 2 : visualKind == 3 ? 3 : 0
var int activeVisualKind = 0
var int activeVisualKey = -999
var float legVisualTop = na
var float legVisualBottom = na
var float activePhaseTop = na
var float activePhaseBottom = na
var box activeLegContainer = na
var box activeLegPhaseFill = na
var array<box> activeLegPhaseFills = array.new_box()
var array<box> visualHistory = array.new_box()
var array<int> visualGroupSizes = array.new_int()
int maxVisualObjects = math.min(460, math.max(40, i_keepSegments * 4))

if barstate.isconfirmed
    bool closeContext = segmentStarts or (not na(activeLegContainer) and visualKind != activeVisualKind)
    if closeContext and not na(activeLegContainer)
        box.set_right(activeLegContainer, bar_index)
        if not na(activeLegPhaseFill)
            box.set_right(activeLegPhaseFill, bar_index)
        array.push(visualHistory, activeLegContainer)
        int archivedObjectCount = 1
        if array.size(activeLegPhaseFills) > 0
            for fillIndex = 0 to array.size(activeLegPhaseFills) - 1
                array.push(visualHistory, array.get(activeLegPhaseFills, fillIndex))
                archivedObjectCount += 1
        array.push(visualGroupSizes, archivedObjectCount)
        while array.size(visualHistory) > maxVisualObjects and array.size(visualGroupSizes) > 0
            int oldestGroupCount = array.shift(visualGroupSizes)
            for deleteIndex = 0 to oldestGroupCount - 1
                if array.size(visualHistory) > 0
                    box.delete(array.shift(visualHistory))
        activeLegContainer := na
        activeLegPhaseFill := na
        array.clear(activeLegPhaseFills)
        legVisualTop := na
        legVisualBottom := na
        activePhaseTop := na
        activePhaseBottom := na
        activeVisualKind := 0
        activeVisualKey := -999

    if not na(activeLegContainer) and visualKind == activeVisualKind and visualKind != 0
        legVisualTop := math.max(legVisualTop, high)
        legVisualBottom := math.min(legVisualBottom, low)
        box.set_right(activeLegContainer, bar_index + 1)
        box.set_top(activeLegContainer, legVisualTop)
        box.set_bottom(activeLegContainer, legVisualBottom)
        if visualKind == 1 or visualKind == -1
            if visualKey != activeVisualKey
                if not na(activeLegPhaseFill)
                    box.set_right(activeLegPhaseFill, bar_index)
                activePhaseTop := high
                activePhaseBottom := low
                float phaseInset = math.min(syminfo.mintick, math.max((activePhaseTop - activePhaseBottom) * 0.005, 0.0))
                color phaseFillColor = f_phaseColor(visualKind, visualPhase)
                activeLegPhaseFill := box.new(left=bar_index, top=activePhaseTop - phaseInset, right=bar_index + 1, bottom=activePhaseBottom + phaseInset, xloc=xloc.bar_index, border_color=color.new(phaseFillColor, 100), border_width=1, bgcolor=color.new(phaseFillColor, f_phaseTransparency(visualPhase)), force_overlay=true)
                array.push(activeLegPhaseFills, activeLegPhaseFill)
                activeVisualKey := visualKey
            else if not na(activeLegPhaseFill)
                activePhaseTop := math.max(activePhaseTop, high)
                activePhaseBottom := math.min(activePhaseBottom, low)
                float phaseInset = math.min(syminfo.mintick, math.max((activePhaseTop - activePhaseBottom) * 0.005, 0.0))
                box.set_right(activeLegPhaseFill, bar_index + 1)
                box.set_top(activeLegPhaseFill, activePhaseTop - phaseInset)
                box.set_bottom(activeLegPhaseFill, activePhaseBottom + phaseInset)
        else if visualKind == 2 and not na(activeLegPhaseFill)
            activePhaseTop := legVisualTop
            activePhaseBottom := legVisualBottom
            float balanceInset = math.min(syminfo.mintick, math.max((legVisualTop - legVisualBottom) * 0.005, 0.0))
            box.set_right(activeLegPhaseFill, bar_index + 1)
            box.set_top(activeLegPhaseFill, legVisualTop - balanceInset)
            box.set_bottom(activeLegPhaseFill, legVisualBottom + balanceInset)
            box.set_bgcolor(activeLegPhaseFill, color.new(C_GREY, balanceVisualPhase == 3 ? 76 : 86))
        color stableFrameColor = visualKind == 1 ? C_BULL_FRAME : visualKind == -1 ? C_BEAR_FRAME : C_GREY
        box.set_border_color(activeLegContainer, stableFrameColor)
        box.set_border_width(activeLegContainer, 1)
        box.set_border_style(activeLegContainer, visualKind == 3 ? line.style_dashed : line.style_solid)
        box.set_bgcolor(activeLegContainer, color.new(C_DARK, 100))
    else if visualKind != 0 and not segmentStarts
        legVisualTop := high
        legVisualBottom := low
        activePhaseTop := high
        activePhaseBottom := low
        color stableStartFrame = visualKind == 1 ? C_BULL_FRAME : visualKind == -1 ? C_BEAR_FRAME : C_GREY
        activeLegContainer := box.new(left=bar_index, top=legVisualTop, right=bar_index + 1, bottom=legVisualBottom, xloc=xloc.bar_index, border_color=stableStartFrame, border_width=1, border_style=visualKind == 3 ? line.style_dashed : line.style_solid, bgcolor=color.new(C_DARK, 100), force_overlay=true)
        array.clear(activeLegPhaseFills)
        if visualKind == 3
            activeLegPhaseFill := na
        else
            float startBandInset = math.min(syminfo.mintick, math.max((activePhaseTop - activePhaseBottom) * 0.005, 0.0))
            color startFillColor = visualKind == 1 or visualKind == -1 ? f_phaseColor(visualKind, visualPhase) : C_GREY
            int startFillTransparency = visualKind == 1 or visualKind == -1 ? f_phaseTransparency(visualPhase) : balanceVisualPhase == 3 ? 76 : 86
            activeLegPhaseFill := box.new(left=bar_index, top=activePhaseTop - startBandInset, right=bar_index + 1, bottom=activePhaseBottom + startBandInset, xloc=xloc.bar_index, border_color=color.new(startFillColor, 100), border_width=1, bgcolor=color.new(startFillColor, startFillTransparency), force_overlay=true)
            array.push(activeLegPhaseFills, activeLegPhaseFill)
        activeVisualKind := visualKind
        activeVisualKey := visualKey
    else
        activeVisualKind := visualKind
        activeVisualKey := -999

if i_livePreviewEnabled and barstate.islast and not barstate.isconfirmed and not segmentStarts and not na(activeLegContainer)
    float previewTop = math.max(legVisualTop, high)
    float previewBottom = math.min(legVisualBottom, low)
    box.set_right(activeLegContainer, bar_index + 1)
    box.set_top(activeLegContainer, previewTop)
    box.set_bottom(activeLegContainer, previewBottom)
    if not na(activeLegPhaseFill)
        float previewBandTop = visualKind == 2 ? previewTop : math.max(activePhaseTop, high)
        float previewBandBottom = visualKind == 2 ? previewBottom : math.min(activePhaseBottom, low)
        float previewInset = math.min(syminfo.mintick, math.max((previewBandTop - previewBandBottom) * 0.005, 0.0))
        box.set_right(activeLegPhaseFill, bar_index + 1)
        box.set_top(activeLegPhaseFill, previewBandTop - previewInset)
        box.set_bottom(activeLegPhaseFill, previewBandBottom + previewInset)

//=============================================================================
// 08 - PRICE-CHART EVENTS
//
// These markers are drawn on the main price chart even though the indicator's
// calculations live in a separate pane. Birth circles mark confirmed leg starts;
// the translucent diamond is only a live preview. An X marks loss of protected
// structure, a square marks accepted balance, and a yellow diamond marks exhaustion.
// The markers identify the event that occurred; they do not predict what price must
// do next. Continuation markers are kept in the oscillator pane to avoid crowding price.
//=============================================================================

plotshape(i_showPriceEvents and bullBirthEvent ? low : na, title="Bullish Drive Birth", style=shape.circle, location=location.absolute, color=C_BULL_DRIVE, size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and bearBirthEvent ? high : na, title="Bearish Drive Birth", style=shape.circle, location=location.absolute, color=C_BEAR_DRIVE, size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_livePreviewEnabled and liveBullPreview ? low : na, title="Live Bull Birth Preview", style=shape.diamond, location=location.absolute, color=color.new(C_BULL_DRIVE, 45), size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_livePreviewEnabled and liveBearPreview ? high : na, title="Live Bear Birth Preview", style=shape.diamond, location=location.absolute, color=color.new(C_BEAR_DRIVE, 45), size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and bullFailureEvent ? bullFailurePrice : na, title="Bullish Structure Failure", style=shape.xcross, location=location.absolute, color=C_BEAR_TREND, size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and bearFailureEvent ? bearFailurePrice : na, title="Bearish Structure Failure", style=shape.xcross, location=location.absolute, color=C_BULL_TREND, size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and balanceBirthEvent ? high : na, title="Balance Formed", style=shape.square, location=location.absolute, color=C_GREY, size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and exhaustionEvent ? exhaustionPrice : na, title="Exhaustion Halo Outer", style=shape.circle, location=location.absolute, color=color.new(C_YELLOW, 88), size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and exhaustionEvent ? exhaustionPrice : na, title="Exhaustion Halo Middle", style=shape.circle, location=location.absolute, color=color.new(C_YELLOW, 62), size=size.tiny, display=display.pane, force_overlay=true)
plotshape(i_showPriceEvents and exhaustionEvent ? exhaustionPrice : na, title="Exhaustion Watch", style=shape.diamond, location=location.absolute, color=C_YELLOW, size=size.tiny, display=display.pane, force_overlay=true)

//=============================================================================
// 09 - OSCILLATOR: ACCEPTED PROGRESS, GIVEBACK, AND PHASE
//
// The positive histogram records the size of new accepted close progress relative
// to the previous best accepted close. No new accepted close means no new progress
// column. Its height is passed through a bounded intensity transform; it is not a
// probability, confidence score, or standalone measure of future return.
//
// The grey field shows how far the current close has given back from the best
// accepted close, scaled by the movement yardstick fixed at leg birth. It describes
// retracement, not a forecast. The lower step track identifies DRIVE, FORMING, TREND,
// or accepted BALANCE. Fixed guide levels keep these three layers visually separated.
//=============================================================================

bool gapPreviewPending = i_resetOnGap and materialGapDetected and not barstate.isconfirmed
float displayClose = i_livePreviewEnabled or barstate.isconfirmed ? close : close[1]
int paneDirection = gapPreviewPending ? 0 : regime == 1 ? 1 : regime == -1 ? -1 : 0
float liveBestClose = bestAcceptedClose
if paneDirection == 1 and not na(bestAcceptedClose)
    liveBestClose := math.max(bestAcceptedClose, displayClose)
else if paneDirection == -1 and not na(bestAcceptedClose)
    liveBestClose := math.min(bestAcceptedClose, displayClose)
float driveImpulseDisplay = not gapPreviewPending ? driveImpulse : na
float givebackUnitsLive = na
if paneDirection == 1 and not na(liveBestClose) and not na(scaleAtBirth) and scaleAtBirth > 0
    givebackUnitsLive := math.max(0.0, (liveBestClose - displayClose) / scaleAtBirth)
else if paneDirection == -1 and not na(liveBestClose) and not na(scaleAtBirth) and scaleAtBirth > 0
    givebackUnitsLive := math.max(0.0, (displayClose - liveBestClose) / scaleAtBirth)
float givebackIntensity = not na(givebackUnitsLive) ? -f_intensity(givebackUnitsLive) : na
float phaseTrack = na
color driveColour = regime == 1 ? f_phaseColor(1, trendPhase) : regime == -1 ? f_phaseColor(-1, trendPhase) : C_GREY
color phaseTrackColour = driveColour
if not gapPreviewPending
    if regime == 1 or regime == -1
        phaseTrack := trendPhase == 1 ? -112.0 : trendPhase == 2 ? -124.0 : -136.0
        phaseTrackColour := f_phaseColor(regime, trendPhase)
    else if regime == 2
        phaseTrack := -148.0
        phaseTrackColour := C_GREY

plot(driveImpulseDisplay, title="Accepted Close Extension", color=driveColour, linewidth=4, style=plot.style_columns, histbase=0, display=display.pane)
plot(i_showGiveback ? givebackIntensity : na, title="Giveback Field", color=color.new(C_GREY, 78), linewidth=1, style=plot.style_area, histbase=0, display=display.pane)
plot(i_showGiveback ? givebackIntensity : na, title="Giveback Boundary", color=C_GREY, linewidth=1, style=plot.style_line, display=display.pane)
plot(phaseTrack, title="Leg Phase Track", color=phaseTrackColour, linewidth=5, style=plot.style_stepline, display=display.pane)
plot(100.0, title="Drive Intensity Ceiling", color=color.new(C_GREY, 87), linewidth=1, display=display.pane)
plot(50.0, title="Drive Reference", color=color.new(C_GREY, 90), linewidth=1, display=display.pane)
plot(0.0, title="Drive Baseline", color=color.new(C_GREY, 38), linewidth=1, display=display.pane)
plot(-50.0, title="Giveback Reference", color=color.new(C_GREY, 90), linewidth=1, display=display.pane)
plot(-100.0, title="Giveback Intensity Ceiling", color=color.new(C_GREY, 87), linewidth=1, display=display.pane)
plot(-150.0, title="Phase Track Base", color=color.new(C_GREY, 90), linewidth=1, display=display.pane)
plotshape(i_showPaneEvents and progressMilestoneEvent ? driveImpulse : na, title="Accepted Progress Milestone", style=shape.diamond, location=location.absolute, color=C_GREY, size=size.tiny, display=display.pane)
plotshape(i_showPaneEvents and bullContinuationEvent ? 0.0 : na, title="Bullish Continuation Event", style=shape.diamond, location=location.absolute, color=C_BULL_FORMING, size=size.tiny, display=display.pane)
plotshape(i_showPaneEvents and bearContinuationEvent ? 0.0 : na, title="Bearish Continuation Event", style=shape.diamond, location=location.absolute, color=C_BEAR_FORMING, size=size.tiny, display=display.pane)
plotshape(i_showPaneEvents and exhaustionEvent ? givebackIntensity : na, title="Exhaustion Event In Pane", style=shape.diamond, location=location.absolute, color=C_YELLOW, size=size.tiny, display=display.pane)
bgcolor(regime == 2 ? color.new(C_GREY, 95) : na, title="Balance Pane Background")

//=============================================================================
// 10 - SCANNER
//
// PHASE reports confirmed direction, while SCAN adds the current directional phase
// or transition/balance context. Hover over SCAN to read the latest confirmed event.
// EXHAUSTION reports the active pullback/recovery state or a latched warning. EXTENSION
// measures movement from leg birth; GIVEBACK measures retracement from the best accepted
// close. Both values are expressed in units of the movement scale frozen at leg birth.
//=============================================================================

var table dashboard = table.new(f_dashboardPosition(i_dashboardPosition), 4, 5, bgcolor=C_DARK, frame_color=color.new(C_GREY, 55), frame_width=1, border_color=color.new(C_GREY, 84), border_width=1, force_overlay=true)
if barstate.isfirst
    table.merge_cells(dashboard, 0, 0, 3, 0)
    table.merge_cells(dashboard, 1, 1, 3, 1)
    table.merge_cells(dashboard, 1, 2, 3, 2)
    table.merge_cells(dashboard, 1, 3, 3, 3)

string marketPhaseText = regime == 1 ? "BULLISH" : regime == -1 ? "BEARISH" : "NEUTRAL"
color marketPhaseColour = regime == 1 ? C_BULL_FORMING : regime == -1 ? C_BEAR_FORMING : C_GREY
string scanText = "SCANNING"
color scanColour = C_GREY
if liveBullPreview and regime != 1
    scanText := "BULLISH DRIVE · PREVIEW"
    scanColour := C_BULL_DRIVE
else if liveBearPreview and regime != -1
    scanText := "BEARISH DRIVE · PREVIEW"
    scanColour := C_BEAR_DRIVE
else if regime == 1 or regime == -1
    scanText := f_phaseName(regime, trendPhase)
    scanColour := f_phaseColor(regime, trendPhase)
else if regime == 2
    scanText := "BALANCE"
    scanColour := C_GREY
else if transitionActive
    scanText := "TRANSITION · SCANNING"
    scanColour := C_GREY

string exhaustionText = "-"
color exhaustionColour = C_MUTED
if regime == 1 or regime == -1
    if exhaustionLatched
        exhaustionText := exhaustionStatus
        exhaustionColour := exhaustionStatus == "STRUCTURE FAILED" ? C_BEAR_TREND : C_YELLOW
    else if pullbackActive
        exhaustionText := "PULLBACK ACTIVE"
        exhaustionColour := C_GREY
    else if legPhase == 1
        exhaustionText := "GIVEBACK"
        exhaustionColour := C_GREY
    else if legPhase == 2
        exhaustionText := "RECOVERY"
        exhaustionColour := f_phaseColor(regime, trendPhase)
    else
        exhaustionText := "NONE"
        exhaustionColour := C_MUTED
else if exhaustionStatus == "STRUCTURE FAILED"
    exhaustionText := exhaustionStatus
    exhaustionColour := C_BEAR_TREND
else if regime == 2
    exhaustionText := "BALANCE"
    exhaustionColour := C_GREY

float extensionUnits = regime == 1 and not na(legBirthClose) and not na(scaleAtBirth) ? (displayClose - legBirthClose) / scaleAtBirth : regime == -1 and not na(legBirthClose) and not na(scaleAtBirth) ? (legBirthClose - displayClose) / scaleAtBirth : na
float givebackUnits = regime == 1 and not na(bestAcceptedClose) and not na(scaleAtBirth) ? math.max(0.0, (bestAcceptedClose - displayClose) / scaleAtBirth) : regime == -1 and not na(bestAcceptedClose) and not na(scaleAtBirth) ? math.max(0.0, (displayClose - bestAcceptedClose) / scaleAtBirth) : na
string extensionText = not na(extensionUnits) and not gapPreviewPending ? str.tostring(extensionUnits, "#.00") + "×" : "-"
string givebackText = not na(givebackUnits) and not gapPreviewPending ? str.tostring(givebackUnits, "#.00") + "×" : "-"

if barstate.islast
    if i_showDashboard
        table.cell(dashboard, 0, 0, "SCANNER", text_color=C_TEXT, bgcolor=C_HEAD, text_size=size.small, text_halign=text.align_center)
        table.cell(dashboard, 0, 1, "PHASE", text_color=C_MUTED, text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 1, 1, marketPhaseText, text_color=marketPhaseColour, bgcolor=color.new(marketPhaseColour, 80), text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 0, 2, "SCAN", text_color=C_MUTED, text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 1, 2, scanText, text_color=scanColour, bgcolor=color.new(scanColour, 82), text_size=size.small, text_halign=text.align_left, tooltip=lastEvent)
        table.cell(dashboard, 0, 3, "EXHAUSTION", text_color=C_MUTED, text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 1, 3, exhaustionText, text_color=exhaustionColour, bgcolor=color.new(exhaustionColour, 84), text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 0, 4, "EXTENSION", text_color=C_MUTED, text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 1, 4, extensionText, text_color=not na(extensionUnits) and extensionUnits < 0 ? C_BEAR_TREND : f_phaseColor(regime, trendPhase), text_size=size.small, text_halign=text.align_right)
        table.cell(dashboard, 2, 4, "GIVEBACK", text_color=C_MUTED, text_size=size.small, text_halign=text.align_left)
        table.cell(dashboard, 3, 4, givebackText, text_color=not na(givebackUnits) and givebackUnits > 0 ? C_GREY : C_MUTED, text_size=size.small, text_halign=text.align_right)
    else
        table.clear(dashboard, 0, 0, 3, 4)

//=============================================================================
// 11 - CONSOLIDATED CONFIRMED ALERT
//
// To receive notifications, create one TradingView alert for this indicator and
// choose "Any alert() function call". Select the event groups you want above; enabled
// events that occur on the same confirmed candle are combined into one message with
// the symbol, timeframe, close, and event names. This avoids a separate alert setup
// for every event type. Live Preview is never included: notifications are generated
// only from confirmed events at bar close. An alert reports a chart event; it is not
// an instruction to buy, sell, open, or close a position.
//=============================================================================

string confirmedEventList = ""
if i_alertBirths and bullBirthEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BULLISH DRIVE BIRTH"
if i_alertBirths and bearBirthEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BEARISH DRIVE BIRTH"
if i_alertPhases and bullFormingEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BULLISH FORMING"
if i_alertPhases and bearFormingEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BEARISH FORMING"
if i_alertPhases and bullTrendEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BULLISH TREND ESTABLISHED"
if i_alertPhases and bearTrendEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BEARISH TREND ESTABLISHED"
if i_alertContinuations and bullContinuationEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "LONG CONTINUATION TRIGGER"
if i_alertContinuations and bearContinuationEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "SHORT CONTINUATION TRIGGER"
if i_alertWarnings and exhaustionEvent
    string exhaustionAlertLabel = regime == 1 ? "BULLISH EXHAUSTION WATCH" : "BEARISH EXHAUSTION WATCH"
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + exhaustionAlertLabel
if i_alertWarnings and bullFailureEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BULLISH STRUCTURE FAILED"
if i_alertWarnings and bearFailureEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BEARISH STRUCTURE FAILED"
if i_alertBalance and balanceBirthEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BALANCE FORMED"
if i_alertBalance and balanceReleaseEvent
    confirmedEventList := confirmedEventList + (str.length(confirmedEventList) > 0 ? " | " : "") + "BALANCE RELEASED"

if i_alertEnabled and barstate.isrealtime and barstate.isconfirmed and str.length(confirmedEventList) > 0
    string alertMessage = "Trend Exhaustion & Continuation Map [BullByte]" + "\nSymbol: " + syminfo.tickerid + "\nTimeframe: " + timeframe.period + "\nClose: " + str.tostring(close, format.mintick) + "\nEvents: " + confirmedEventList
    alert(alertMessage, alert.freq_once_per_bar_close)
